<?php
// +-------------------------------------------------------------------------+
// | PHP Version 5                                                           |
// +-------------------------------------------------------------------------+
// | ImaEngine - web development framework                                   |
// | Copyright (c) 2006-2007 Sardar Yumatov <ja.doma@gmail.com>              |
// +-------------------------------------------------------------------------+
// | This library is free software; you can redistribute it and/or modify it |
// | under the terms of the GNU Lesser General Public License as published   |
// | by the Free Software Foundation; either version 2.1 of the License, or  |
// | (at your option) any later version.                                     |
// |                                                                         |
// | This library is distributed in the hope that it will be useful, but     |
// | WITHOUT ANY WARRANTY; without even the implied warranty of              |
// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.                    |
// | See the GNU Lesser General Public License for more details.             |
// |                                                                         |
// | You should have received a copy of the GNU Lesser General Public        |
// | License along with this library; if not, write to the Free Software     |
// | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
// +-------------------------------------------------------------------------+

/**
* File contains localized template name scheme implementation.
*
* Name schemes implemented here support skins as simple subdirectories
* from the base directory. However mapping between skin name and skin
* sub-path should be clearly defined in configuration for simplicity and
* safety reasons.
*
* Example of use:
* <code>
*   $provider = new LocalizedTemplateDocumentProvider(); //default paths from configuration
*   $tpl = $provider->getTemplate('test/template'); //default skin, current interface locale, compiling templater by default
*   $tpl->processOutput($handler);
* </code>
*
* Configuration used:
*  - [template]dir:source-base-dir -- directory where all templates reside by default
*  - [template]dir:compiled-base-dir -- directory where all compiled templates reside by default, may be equal to source dir
*  - [template]str:current-skin -- current skin name
*  - [template]str:skin.* -- mapping of the skin name to relative skin subdirectory from the *-base-dir
*  - [template]boolean:flat-mode -- enable flat mode, default hardcoded <b>false</b>
*
* @author Sardar Yumatov <ja.doma@gmail.com>
* @copyright Copyright &copy; 2006-2007 Sardar Yumatov
* @version v1.0
* @package common.template
* @category Templater
*
* @PHP5only
*/

if(!defined('PROCESS')) {
	header('HTTP/1.1 404 Not Found');
	die('NotFound');
}

require_once('lib/GenericExceptions.php');
require_once('lib/FileUtils.php');
require_once('lib/L10N/Localization.php');
require_once('lib/Configuration.php');
require_once('lib/template/AbstractTemplateDocument.php');

//loaded on demand
//require_once('lib/template/CompiledTemplateDocument.php');
//require_once('lib/template/ParsedTemplateDocument.php');

/**
 * Template loader/provider interface.
 *
 * This class is able to build localized path to the source template and create a known handler
 * to process the template.
 *
 * All templates are accessible by name which is a template file name wihtout extension and
 * localization info. This provider tries to find best suitable localized template or fallback
 * to non localized template. For the compiling template the localized destination path will
 * also be crated.
 *
 * Constructor of this class and all template load methods accept array with parameters:
 *   - type -- template type: 'compiling'(default) or 'parsing' templater.
 *   - basedir -- base dir where all templates reside, config <b>[template]dir:source-base-dir</b> if not set.
 *   - basedir.compiled -- base dir where compiled templates will be saved, config <b>[template]dir:compiled-base-dir</b> if not set or 'basedir' if config is not available.
 *   - locale -- locale/language to use, current interface locale by default
 *   - skin -- skin name to use, config <b>[template]str:current-skin</b> if not set
 *   - flat -- enable flat mode (no language subdirectories)
 *
 * All parameters have their config or hardcoded defaults.
 * The skin name must have matching config definition <b>[template]str:skin.*</b> or exception will
 * be thrown. Config returns subdirectory from the 'basedir' where in template files reside.
 *
 * This provider builds path: <b>basedir/skin/language/name_path/locale.template.tpl</b>
 * The 'language' directory will not be used if 'flat' mode is set. The 'name_path' is defined
 * in template name which is any relative path from resolved skin directory. The template file
 * will be prefixed with best matching 'locale'. Extension for template files is '.tpl',
 * compiled templates have '.btpl' as extension.
 *
 * If localized version is not found, then non localized one will be used. Non localized template
 * doesn't contain best matching 'locale' as file name prefix. Read more info about localization in
 * <tt>Localization</tt> classdoc.
 *
 * @see Localization
 * @author Sardar Yumatov <ja.doma@gmail.com>
 */
class LocalizedTemplateDocumentProvider implements TemplateDocumentProvider {

	/** Current provider configuration
	 * @var array */
	private $params;

	/** Default provider instance with default settings
	 * @var LocalizedTemplateDocumentProvider */
	private static $instance;

	/**
	 * Construct new provider.
	 * The parameters define defaults used later by <tt>getTemplate()</tt>.
	 * @param array $params
	 */
	public function __construct($params = null) {
		$this->params = self::mergeConfig($params, null);
	}

	/**
	 * Merges configuration parameters.
	 * Method merges all non-null parameters from $source with $mydef. Everything that is still
	 * not defined will be taken from defaults and <tt>Configuration</tt>
	 * @param array $source given parameters, most priority
	 * @param array $mydef current configuration, used if key is not defined in $source
	 * @return array fully resolved configuration values as defined in classdoc
	 */
	protected static function mergeConfig($source, $mydef) {
		global $locale;
		static $keys = array('type', 'basedir', 'basedir.compiled', 'locale', 'skin', 'flat');
		$ret = array(); $source = is_null($source)? array(): $source; //to disable warning
		foreach ($keys as $k) {
			if(isset($source[$k])) $ret[$k] = $source[$k];
			elseif(!key_exists($k, $source) && isset($mydef[$k])) $ret[$k] = $mydef[$k]; //null in $source means 'use default'
			else { //fetch from config
				switch ($k) {
					case 'type': $ret[$k] = 'compiling'; break;
					case 'basedir': $ret[$k] = Configuration::getOption('[template]dir:source-base-dir'); break;
					case 'basedir.compiled': $ret[$k] = Configuration::get('[template]dir:compiled-base-dir', (isset($ret['basedir'])? $ret['basedir']: Configuration::getOption('[template]dir:source-base-dir'))); break;
					case 'skin': $ret[$k] = Configuration::getOption('[template]str:current-skin'); break;
					case 'flat': $ret[$k] = Configuration::get('[template]boolean:flat-mode', false); break;
					case 'locale': $ret[$k] = $locale['locale'];
				}
			}
		}
		if($ret['type'] != 'compiling' && $ret['type'] != 'parsing') throw new InvalidArgumentException("Wrong type parameter '{$ret['type']}', should be either 'compiling' or 'parsing'.");
		$ret['flat'] = (boolean)$ret['flat'];
		$ret['basedir'] = FileUtils::ensureDir($ret['basedir']); //throws InstallationException
		$ret['basedir.compiled'] = $ret['type'] == 'compiling'? FileUtils::ensureDir($ret['basedir.compiled']): null; //throws InstallationException
		return $ret;
	}

	/**
	 * Returns current provider configuration.
	 * All keys of returned associative array ara defined in top classdoc.
	 * @return array configuration as list of option=>value
	 */
	public function getConfiguration() {
		return $this->params;
	}

	/**
	 * Load and return template by $name.
	 *
	 * Method merges current provider parameters with given parameters and loads the template.
	 * The null value means default, not the current provider configuration. For example if
	 * 'locale' parameter is omitted, then one defined in provider will be used. If parameter
	 * is set, but contains <b>null</b>, then current interface locale is used regardless of
	 * current provider configuration.
	 *
	 * <b>Note:</b> new provider will be created if $params are different than current provider configuration.
	 * This memorized config will then be used by including implates within this template.
	 *
	 * @throws NotFoundException if source template isn't found
	 * @throws IOException on any IO error
	 * @throws SyntaxException on any syntax exception while loading/compiling the source template
	 *
	 * @param string $name template name-path, may be any relative path from the skin directory without extension
	 * @param array $params associative array with optional parameters as defined in top classdoc
	 * @return LocalizedTemplateDocument loaded document
	 */
	public function getTemplate($name, $params=null) {
		$params = self::mergeConfig($params, $this->params);

		$loc = Localization::parseLocale($params['locale']); //throws InvalidArgumentException
		$fskin = FilePath::createPath(Configuration::getOption("[template]str:skin.{$params['skin']}")); //throws InvalidArgumentException and ConfigurationException
		if($fskin->isUnsafe() || $fskin->isAbsolute(true)) throw new InstallationException("Skin directory defined in '[template]skin.{$params['skin']}' is either absolute or unsafe!");

		$compiling = ($params['type'] == 'compiling');
		$base = FilePath::createPath($fskin, true, $params['basedir'], true);

		$compiled_base = $compiling? FilePath::createPath($fskin, true, $params['basedir.compiled'], true): null;
		if(!$base->getHandler()->exists() || ($compiling && !$compiled_base->getHandler()->exists())) throw new InstallationException('Skin directory \''.$base->getPath().'\''.($compiling? 'or compiled skin dir \''.$compiled_base->getPath().'\'': '').' doesn\'t exist!');

		//decomposite name
		$fname = FilePath::createPath($name.'.tpl');
		if($fname->isUnsafe() || $fname->isAbsolute(true)) throw new InvalidArgumentException("Given template name path '{$name}' is either absolute or unsafe!");
		$name = substr($fname->getPath(true), 2, -4); //remove ./ and .tpl from the resolved  path


		try { //try to find source file
			$template = Localization::getLocalizedResource($fname, $base, $params['flat'], $loc); //throws NotFoundException
		} catch (NotFoundException $e) { //try non localized one
			if(!is_file($template = FileUtils::absolutePath($fname, $base, true))) throw new NotFoundException("Template '{$name}' doesn't exist! Both localized and non localized are probed. Path: '{$template}'.");
			trigger_error("Localized template '{$name}' was not found, falling back to non localizded: '{$template}'. Localization: {$e->getMessage()}", E_USER_NOTICE);
		}
		//build destination file
		if($compiling) {
			require_once('lib/template/CompiledTemplateDocument.php');
			$compiled_template = Localization::createLocalizedResource($name.'.btpl', $compiled_base, $params['flat'], $loc);
			$tpl = new CompiledTemplateDocument($name, $template, $compiled_template);
		} else {
			require_once('lib/template/ParsedTemplateDocument.php');
			$tpl = new ParsedTemplateDocument($name, $template);
		}

		$prov = ($params != $this->params)? new LocalizedTemplateDocumentProvider($params): $this;
		$tpl->setProvider($prov);
		return $tpl;
		//return new LocalizedTemplateDocument($prov, $tpl, $loc);
	}


	/**
	 * Returns localized <tt>ParsedTemplateDocument</tt> templater.
	 * This method overrides <b>type</b> key in $params by 'parsing' and calls <tt>getTemplate()</tt>
	 *
	 * @throws NotFoundException if source template isn't found
	 * @throws IOException on any IO error
	 * @throws SyntaxException on any syntax exception while loading the source template
	 *
	 * @param string $name template name path
	 * @param array $params optional parameters with locale and skin info
	 * @return LocalizedTemplateDocument wrapper over parsing templater
	 */
	public function getParsingTemplate($name, $params=null) {
		$p = is_null($params)? array(): $params;
		$p['type'] = 'parsing';
		return $this->getTemplate($name, $p);
	}

	/**
	 * Returns localized <tt>CompiledTemplateDocument</tt> templater.
	 * This method overrides <b>type</b> key in $params by 'compiling' and calls <tt>getTemplate()</tt>
	 *
	 * @throws NotFoundException if source template isn't found
	 * @throws IOException on any IO error
	 * @throws SyntaxException on any syntax exception while loading/compiling the source template
	 *
	 * @param string $name template name path
	 * @param array $params optional parameters with locale and skin info
	 * @return LocalizedTemplateDocument wrapper over compiling templater
	 */
	public function getCompilingTemplate($name, $params=null) {
		$p = is_null($params)? array(): $params;
		$p['type'] = 'compiling';
		return $this->getTemplate($name, $p);
	}

	/**
	 * Returns true if given $provider is a <tt>LocalizedTemplateDocumentProvider</tt> and is equaly configured.
	 * Equaly configured providers will return the same template if no other options are provided
	 * while obtaing template.
	 * @param TemplateDocumentProvider $provider another provider to compare
	 * @return boolean true if both providers are of the same type and equaly configured
	 */
	public function isEqual(TemplateDocumentProvider $provider) {
		return ($provider instanceof LocalizedTemplateDocumentProvider && $provider->getConfiguration() == $this->params);
	}

	/**
	 * Returns provider instance with default settings.
	 * Use this instance if you doesn't need a special configured provider.
	 * @return LocalizedTemplateDocumentProvider deafult provider
	 */
	public static function getInstance() {
		if(!isset(self::$instance)) self::$instance = new LocalizedTemplateDocumentProvider();
		return self::$instance;
	}
}

?>